# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2007 Renato Chencarek <renato.chencarek@gmail.com>
# All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# Author: Renato Chencarek <renato.chencarek@gmail.com>

from trac.core import *
from trac.util import escape
from trac.mimeview.api import IContentConverter

from trac.mimeview import *
from trac.wiki.api import WikiSystem
from trac.util.html import escape, Markup, Element, html
from trac.util.text import shorten_line, to_unicode

from trac.wiki.formatter import *
from trac.wiki.formatter import WikiProcessor

from tempfile import *
import glob

from StringIO import StringIO

def system_message(msg, text=None):
    return os.linesep + text + os.linesep


import os
import re

class TexFormatter(Formatter):

    def __init__(self, env, req, absurls=False, db=None, begindoc=False):
        Formatter.__init__(self, env, req)

        self.latex_rules = []
        self.latex_rules.append(r"(?P<latexescape>[&{}_=#%])")
        pattern = self.wiki.rules.pattern
        n = len(pattern)
        p1 = "|".join(self.latex_rules)
        p2 = pattern[3:][:-1]
        pattern = '(?:' + p2 + "|" + p1 + ')'
        self.my_rules = re.compile(pattern)
        self.open_tags = []
        self.list_depth = 0

        self.absurls = False
        self.begindoc = begindoc
        self.enddoc = False

    def _bolditalic_formatter(self, match, fullmatch):
        italic = ('\emph{', '}')
        italic_open = self.tag_open_p(italic)
        tmp = ''
        if italic_open:
            tmp += italic[1]
            self.close_tag(italic[1])
        tmp += self._bold_formatter(match, fullmatch)
        if not italic_open:
            tmp += italic[0]
            self.open_tag(*italic)
        return tmp

    def _bold_formatter(self, match, fullmatch):
        return self.simple_tag_handler(match, r'\textbf{', '}')

    def _italic_formatter(self, match, fullmatch):
        return self.simple_tag_handler(match, r'\emph{', '}')

    def _underline_formatter(self, match, fullmatch):
        return self.simple_tag_handler(match, r'\underline{','}')

    def _strike_formatter(self, match, fullmatch):
        return self.simple_tag_handler(match, r'\sout{', '}')

    def _subscript_formatter(self, match, fullmatch):
        return self.simple_tag_handler(match, r'$_{', '}$')

    def _superscript_formatter(self, match, fullmatch):
        return self.simple_tag_handler(match, r'$^{', '}$')

    def _inlinecode_formatter(self, match, fullmatch):
        return self.escape_verb(fullmatch.group('inline'))

    def _inlinecode2_formatter(self, match, fullmatch):
        return self.escape_verb(fullmatch.group('inline2'))

    # LATEX and HTML escape of &, < and >

    def escape_verb(self, match):
        sep = "|"
        for p in "|!^*$@":
            if p not in match:
                sep = p
                break

        result = self._latexescape_formatter(match, None)

        return r"\texttt%s%s%s" % ("{", result, "}")

    def _latexescape_formatter(self, match, fullmatch):
        for c in "><=":
            if c in match:
                match = match.replace('%s' % c,"$%s$" % c)
        for c in "&{}_#%":
            if c in match:
                match = match.replace('%s' % c,"\\%s" % c)

        return match

    def _latexescape_comments(self, match, fullmatch):
        match = match.replace('%', "\\%")
        return match

    def _htmlescape_formatter(self, match, fullmatch):
        return self._latexescape_formatter( match, fullmatch)

    # Links

    def _format_link(self, formatter, ns, page, label, ignore_missing):
        page, query, fragment = formatter.split_link(page)
        href = formatter.href.wiki(page) + fragment
        return self.escape_verb(label)

    def _lhref_formatter(self, match, fullmatch):
        rel = fullmatch.group('rel')
        ns = fullmatch.group('lns')
        target = self._unquote(fullmatch.group('ltgt'))
        label = fullmatch.group('label')
        if not label: # e.g. `[http://target]` or `[wiki:target]`
            if target:
                if target.startswith('//'): # for `[http://target]`
                    label = ns+':'+target   #  use `http://target`
                else:                       # for `wiki:target`
                    return self.escape_verb(target) #  use only `target`
            else: # e.g. `[search:]`
                return ""
        else:
            label = self._unquote(label)
        if rel:
            return self._make_relative_link(rel, label or rel)
        else:
            return self._make_link(ns, target, match, label)


    def _make_interwiki_link(self, ns, target, label):
        from trac.wiki.interwiki import InterWikiMap
        interwiki = InterWikiMap(self.env)
        if ns in interwiki:
            url, title = interwiki.url(ns, target)
            return self._make_ext_link(url, label, title)
        else:
            return None


    def _make_link(self, ns, target, match, label):
        # first check for an alias defined in trac.ini
        ns = self.env.config.get('intertrac', ns) or ns

        if ns in self.wiki.link_resolvers:
            return self.escape_verb(label)
        elif target.startswith('//') or ns == "mailto":
            return self._make_ext_link(ns+':'+target, label)
        else:

            return self._make_intertrac_link(ns, target, label) or \
                   self._make_interwiki_link(ns, target, label) or \
                   match

    def _make_ext_link(self, url, text, title=''):
        text = self._latexescape_comments(text, None)
        url = self._latexescape_comments(url, None)
        if text == url:
            return r"\url{%s}" % url
        else:
            return r"\href{%s}{%s}" % (url, text)

    def _make_relative_link(self, url, text):
        text = self._latexescape_comments(text, None)
        url = self._latexescape_comments(url, None)
        if text == url:
            return r"\url{%s}" % url
        else:
            return r"\hyperref[%s]{%s}" % (url.lstrip("#"), text)


    # Headings

    def _parse_heading(self, match, fullmatch, shorten):
        match = match.strip()

        depth = min(len(fullmatch.group('hdepth')), 5)
        anchor = fullmatch.group('hanchor') or ''
        heading_text = match[depth+1:-depth-1-len(anchor)]
        heading = wiki_to_oneliner(heading_text, self.env, self.db, False,
                                   self._absurls)
        if anchor:
            anchor = anchor[1:]
        else:
            sans_markup = heading.plaintext(keeplinebreaks=False)
            anchor = self._anchor_re.sub('', sans_markup)
            if not anchor or anchor[0].isdigit() or anchor[0] in '.-':
                # an ID must start with a Name-start character in XHTML
                anchor = 'a' + anchor # keeping 'a' for backward compat
        i = 1
        anchor_base = anchor
        while anchor in self._anchors:
            anchor = anchor_base + str(i)
            i += 1
        self._anchors[anchor] = True
        if shorten:
            heading = wiki_to_oneliner(heading_text, self.env, self.db, True,
                                       self._absurls)
        return (depth, heading, anchor)

    def _heading_formatter(self, match, fullmatch):
        self.close_table()
        self.close_paragraph()
        self.close_indentation()
        self.close_list()
        self.close_def_list()
        depth, heading, anchor = self._parse_heading(match, fullmatch, False)

        if self.begindoc:
            self.out.write(os.linesep + ('\%ssection{%s}' % ("sub" * (depth - 1), heading)) + os.linesep)

    # Generic indentation (as defined by lists and quotes)

    def _set_tab(self, depth):
        """Append a new tab if needed and truncate tabs deeper than `depth`

        given:       -*-----*--*---*--
        setting:              *
        results in:  -*-----*-*-------
        """
        tabstops = []
        for ts in self._tabstops:
            if ts >= depth:
                break
            tabstops.append(ts)
        tabstops.append(depth)
        self._tabstops = tabstops

    # Paragraphs

    def open_paragraph(self):
        if not self.paragraph_open:
            if self.begindoc:
                self.out.write(os.linesep)

            self.paragraph_open = 1

    def close_paragraph(self):
        if self.paragraph_open:
            while self._open_tags != []:
                if self.begindoc:
                    self.out.write(self._open_tags.pop()[1])
            self.paragraph_open = 0

    # Definition Lists

    def _definition_formatter(self, match, fullmatch):
        if self.in_def_list:
            tmp = ""
        else:
            tmp = r'\begin{description}' + os.linesep + os.linesep

        definition = match[:match.find('::')]
        tmp += r'\item{%s}' % wiki_to_oneliner(definition, self.env, self.db)
        self.in_def_list = True
        return tmp

    def close_def_list(self):
        if self.in_def_list:
            if self.begindoc:
                self.out.write( os.linesep + r'\end{description}' + os.linesep)
        self.in_def_list = False

    # Blockquote

    def _set_quote_depth(self, depth, citation=False):
        def open_quote(depth):
            self.close_table()
            self.close_paragraph()
            self.close_list()
            def open_one_quote(d):
                self._quote_stack.append(d)
                self._set_tab(d)
                class_attr = citation and ' class="citation"' or ''
            if citation:
                for d in range(quote_depth+1, depth+1):
                    open_one_quote(d)
            else:
                open_one_quote(depth)
        def close_quote():
            self.close_table()
            self.close_paragraph()
            self._quote_stack.pop()

        quote_depth = self._get_quote_depth()
        if depth > quote_depth:
            self._set_tab(depth)
            tabstops = self._tabstops[::-1]
            while tabstops:
                tab = tabstops.pop()
                if tab > quote_depth:
                    open_quote(tab)
        else:
            while self._quote_stack:
                deepest_offset = self._quote_stack[-1]
                if depth >= deepest_offset:
                    break
                close_quote()
            if not citation and depth > 0:
                if self._quote_stack:
                    old_offset = self._quote_stack[-1]
                    if old_offset != depth: # adjust last depth
                        self._quote_stack[-1] = depth
                else:
                    open_quote(depth)
        if depth > 0:
            self.in_quote = True

    # Table

    def _last_table_cell_formatter(self, match, fullmatch):
        return ''

    def _table_cell_formatter(self, match, fullmatch):
        self.open_table()
        self.open_table_row()
        self.current_cols += 1
        if self.current_cols > self.max_cols:
            self.max_cols = self.current_cols
        if self.in_table_cell:
            return ' & '
        else:
            self.in_table_cell = 1
            return ''

    def open_table(self):
        if not self.in_table:
            self.close_paragraph()
            self.close_list()
            self.close_def_list()
            self.in_table = 1
            self.max_cols = 0
            if self.begindoc:
                self.out.write(os.linesep + r'\mytable{%' + os.linesep)

    def open_table_row(self):
        if not self.in_table_row:
            self.open_table()
            self.in_table_row = 1
            self.current_cols = 0
            if self.begindoc:
                self.out.write(r'\hline' + os.linesep )

    def close_table_row(self):
        if self.in_table_row:
            self.in_table_row = 0
            self.in_table_cell = 0
            if self.current_cols > self.max_cols:
                self.max_cols = self.current_cols
            if self.begindoc:
                self.out.write(r"\tabularnewline")

    def close_table(self):
        if self.in_table:
            self.close_table_row()
            if self.begindoc:
                self.out.write(r'\hline' + os.linesep)
                self.out.write(r'}{|%s}{%d}' % ("K|" * self.max_cols, self.max_cols) + os.linesep)
            self.in_table = 0

    # Lists

    def _list_formatter(self, match, fullmatch):
        ldepth = len(fullmatch.group('ldepth'))
        listid = match[ldepth]
        self.in_list_item = True
        class_ = start = None
        if listid in '-*':
            type_ = 'ul'
        else:
            type_ = 'ol'
            idx = '01iI'.find(listid)
            if idx >= 0:
                class_ = ('arabiczero', None, 'lowerroman', 'upperroman')[idx]
            elif listid.isdigit():
                start = match[ldepth:match.find('.')]
            elif listid.islower():
                class_ = 'loweralpha'
            elif listid.isupper():
                class_ = 'upperalpha'
        self._set_list_depth(ldepth, type_, class_, start)
        return ''

    def _get_list_depth(self):
        """Return the space offset associated to the deepest opened list."""
        return self._list_stack and self._list_stack[-1][1] or 0

    def _set_list_depth(self, depth, new_type, list_class, start):
        def open_list():
            self.close_table()
            self.close_paragraph()
            self.close_indentation() # FIXME: why not lists in quotes?
            self._list_stack.append((new_type, depth))
            self._set_tab(depth)
            class_attr = list_class and ' class="%s"' % list_class or ''
            start_attr = start and ' start="%s"' % start or ''
            if self.begindoc:
                self.out.write (r'\begin{itemize}' + os.linesep)

            self.list_depth += 1
            if self.begindoc:
                self.out.write('\\item ')

        def close_list(tp):
            self._list_stack.pop()
            if self.begindoc:
                self.out.write (r'\end{itemize}' + os.linesep)

            self.list_depth -= 1

        # depending on the indent/dedent, open or close lists
        if depth > self._get_list_depth():
            open_list()
        else:
            while self._list_stack:
                deepest_type, deepest_offset = self._list_stack[-1]
                if depth >= deepest_offset:
                    break
                close_list(deepest_type)
            if depth > 0:
                if self._list_stack:
                    old_type, old_offset = self._list_stack[-1]
                    if new_type and old_type != new_type:
                        close_list(old_type)
                        open_list()
                    else:
                        if old_offset != depth: # adjust last depth
                            self._list_stack[-1] = (old_type, depth)
                        if self.begindoc:
                            self.out.write('\\item ')
                else:
                    open_list()

    def close_list(self):
        self._set_list_depth(0, None, None, None)

    # Code blocks

    def handle_code_block(self, line):
        if line.strip() == Formatter.STARTBLOCK:
            self.in_code_block += 1
            if self.in_code_block == 1:
                self.code_processor = None
                self.code_text = ''
            else:
                self.code_text += line + os.linesep
        elif line.strip() == Formatter.ENDBLOCK:
            self.in_code_block -= 1
            if self.in_code_block == 0:
                self.close_table()
                self.close_paragraph()
                if self.code_processor:
                    language = self.code_processor
                    if language == "Latex":
                        if  self.code_text.startswith(Formatter.STARTBLOCK):
                            if self.begindoc:
                            	self.out.write(r'\begin{verbatim}' + os.linesep + self.code_text + os.linesep + r'\end{verbatim}')
                        else:
                            if self.begindoc:
                                self.out.write(self.code_text)
                    else:
                        body = r'\begin{lstlisting}' + os.linesep + self.code_text + os.linesep + r'\end{lstlisting}'
                        if self.begindoc:
                            self.out.write(r'''
\lstset{language=%s,
frame=single,
showstringspaces=false,
extendedchars=true,
backgroundcolor=\color[rgb]{0.95,0.95,0.95},
rulecolor=\color[rgb]{0.3,0.3,0.3},
basicstyle=\small\upshape\ttfamily,
commentstyle=\color[rgb]{0.5,0.0,0.0}\rmfamily\itshape,
keywordstyle=\color[rgb]{0.7,0.0,0.8}\bfseries,
stringstyle=\color[rgb]{0.6,0.4,0.4},
identifierstyle=\color[rgb]{0.2,0.2,0.9}
}''' % language + os.linesep + body + os.linesep)
                else:
                    if self.begindoc:
                        self.out.write(r'\begin{verbatim}' + os.linesep + self.code_text + os.linesep + r'\end{verbatim}')
            else:
                self.code_text += line + os.linesep
        elif not self.code_processor:
            match = Formatter._processor_re.search(line)
            if match:
                name = match.group(1)
                self.code_processor = name
            else:
                self.code_text += line + os.linesep
                self.code_processor = None
        else:
            self.code_text += line + os.linesep

    def close_code_blocks(self):
        while self.in_code_block > 0:
            self.handle_code_block(Formatter.ENDBLOCK)

    # WikiMacros

    def _macro_formatter(self, match, fullmatch):
        name = fullmatch.group('macroname')
        if name.lower() == 'latexdoccls':
            self.begindoc = True
        if name.lower() == 'latexenddoc':
            self.enddoc = True

        if name.lower() == 'br':
            return os.linesep + r'\\' + os.linesep
        name = 'Tex' + name
        args = fullmatch.group('macroargs')
        try:
            macro = WikiProcessor(self.env, name)
            return macro.process(self.req, args, True)
        except Exception, e:
            self.env.log.error('Macro %s(%s) failed' % (name, args), exc_info=True)
            return None #system_message("Macro %s not found" % name)

    # -- Wiki engine

    def handle_match(self, fullmatch):
        for itype, match in fullmatch.groupdict().items():
            if match and not itype in self.wiki.helper_patterns:
                # Check for preceding escape character '!'
                if match[0] == '!':
                    return self.escape_verb(escape(match[1:]))
                if itype in self.wiki.external_handlers:
                    external_handler = self.wiki.external_handlers["i0"]
                    tmp = self.wiki._format_link
                    self.wiki._format_link = self._format_link
                    ret =  external_handler(self, match, fullmatch)
                    self.wiki._format_link = tmp
                    return ret
                else:
                    internal_handler = getattr(self, '_%s_formatter' % itype)
                    return internal_handler(match, fullmatch)


    def format(self, text, out=None, escape_newlines=False):
        self.reset(out)
        for line in text.splitlines():
            # Handle code block
            if self.in_code_block or line.strip() == Formatter.STARTBLOCK:
                self.handle_code_block(line)
                continue
            # Handle Horizontal ruler
            elif line[0:4] == '----':
                self.close_table()
                self.close_paragraph()
                self.close_indentation()
                self.close_list()
                self.close_def_list()
                if self.begindoc:
                    self.out.write(os.linesep + r'\rule{\textwidth}{1pt}' + os.linesep)
                continue
            # Handle new paragraph
            elif line == '':
                self.close_paragraph()
                self.close_indentation()
                self.close_list()
                self.close_def_list()
                continue

            # Tab expansion and clear tabstops if no indent
            line = line.replace('\t', ' '*8)
            if not line.startswith(' '):
                self._tabstops = []

            if escape_newlines:
                line += r' \\'
            self.in_list_item = False
            self.in_quote = False
            # Throw a bunch of regexps on the problem

            result = re.sub(self.my_rules, self.replace, line)

            if not self.in_list_item:
                self.close_list()

            if not self.in_quote:
                self.close_indentation()

            if self.in_def_list and not line.startswith(' '):
                self.close_def_list()

            if self.in_table and line.strip()[0:2] != '||':
                self.close_table()

            if len(result) and not self.in_list_item and not self.in_def_list \
                    and not self.in_table:
                self.open_paragraph()

            if self.begindoc:
                self.out.write(result + os.linesep)

            self.close_table_row()

            if self.enddoc:
                self.enddoc = True
                self.begindoc = False

        self.close_table()
        self.close_paragraph()
        self.close_indentation()
        self.close_list()
        self.close_def_list()
        self.close_code_blocks()

class OneLinerTexFormatter(TexFormatter):
    """
    A special version of the wiki formatter that only implement a
    subset of the wiki formatting functions. This version is useful
    for rendering short wiki-formatted messages on a single line
    """
    flavor = 'oneliner'

    def __init__(self, env, absurls=False, db=None):
        TexFormatter.__init__(self, env, None, absurls, db)

    def _macro_formatter(self, match, fullmatch):
        name = fullmatch.group('macroname')
        if name.lower() == 'br':
            return os.linesep
        elif name == 'comment':
            return '%'
        else:
            args = fullmatch.group('macroargs')
            return '[[%s%s]]' % (name,  args and '(...)' or '')

    def format(self, text, out, shorten=False):
        if not text:
            return
        self.out = out
        self._open_tags = []

        # Simplify code blocks
        in_code_block = 0
        processor = None
        buf = StringIO()
        for line in text.strip().splitlines():
            if line.strip() == Formatter.STARTBLOCK:
                in_code_block += 1
            elif line.strip() == Formatter.ENDBLOCK:
                if in_code_block:
                    in_code_block -= 1
                    if in_code_block == 0:
                        if processor != 'comment':
                            buf.write(' ![...]' + os.linesep)
                        processor = None
            elif in_code_block:
                if not processor:
                    if line.startswith('#!'):
                        processor = line[2:].strip()
            else:
                buf.write(line + os.linesep)
        result = buf.getvalue()[:-1]

        if shorten:
            result = shorten_line(result)

        result = re.sub(self.my_rules, self.replace, result)
        result = result.replace('[...]', '[&hellip;]')
        if result.endswith('...'):
            result = result[:-3] + '&hellip;'

        # Close all open 'one line'-tags
        result += self.close_tag(None)
        # Flush unterminated code blocks
        if in_code_block > 0:
            result += '[&hellip;]'
        out.write(result)

def wiki_to_oneliner(wikitext, env, db=None, shorten=False, absurls=False, req=None):
    if not wikitext:
        return Markup()
    out = StringIO()
    OneLinerTexFormatter(env, absurls, db).format(wikitext, out, shorten)
    return Markup(out.getvalue())

def latex_default_header():
    return ""

def latex_default_end():
    return ""

class Trac2LATEXPlugin(Component):
    """Convert Wiki pages to LATEX using HTML2LATEX"""
    implements(IContentConverter)

    def clean_tmp_dir(self, tempdir):
        for f in glob.glob(os.path.join(tempdir ,"*")):
            os.unlink(f)
        os.rmdir(tempdir)

    def read_log(self, tempdir):
        fin = open(tempdir + "/log", "r")
        page = fin.read()
        fin.close()
        return page

    # IContentConverter methods
    def get_supported_conversions(self):
        yield ('tex', 'Latex', 'tex', 'text/x-trac-wiki', 'application/tex', 7)
        yield ('pdf', 'Latex-Pdf', 'pdf', 'text/x-trac-wiki', 'application/x-pdf', 7)

    def convert_content(self, req, input_type, source, output_type):
        try:
            self.env.log.debug('Trac2LATEX starting')
            tempdir = mkdtemp(prefix="trac2latex")
            self.env.tempdir = tempdir
            codepage = self.env.config.get('trac', 'charset', 'ISO8859-15')
            page = source.encode(codepage, 'ignore')

            out = StringIO()

            TexFormatter(self.env, req).format(source, out, False)
            page = latex_default_header()
            page += out.getvalue().encode(codepage)
            page += latex_default_end()

            if "tex" in output_type:
                self.clean_tmp_dir(tempdir)
                return (page, 'application/tex')

            try:
                fd, tmpfile = mkstemp(dir=tempdir, prefix='trac2latex')
                fout = open(tmpfile + ".tex", "w")
                fout.write(page)
                fout.close()
            except Exception, e:
                print "ERROR: Cannot create temporary file %s" % (tmpfile + ".tex")
                return ("ERROR: Cannot create temporary file %s" % (tmpfile + ".tex"), 'text/html')

            try:
                curdir = os.getcwd()
                os.chdir(tempdir)
                os.system("pdflatex -interaction=nonstopmode " + tmpfile + ".tex > " + tempdir + "/log")
                os.system("bibtex " + tmpfile + "  >> " + tempdir + "/log")
                os.system("pdflatex -interaction=nonstopmode " + tmpfile + ".tex >> " + tempdir + "/log") 
                os.system("pdflatex -interaction=nonstopmode " + tmpfile + ".tex >> " + tempdir + "/log")
                os.chdir(curdir)
                fin = open(tmpfile + ".pdf", "r")
                page = fin.read()
                fin.close()
            except Exception, e:
                print "LATEX ERROR: %s: %s " % (e, self.read_log(tempdir))
                return ("LATEX ERROR: %s: %s " % (e, self.read_log(tempdir)), 'text/html')

            self.clean_tmp_dir(tempdir)

            return (page, 'application/x-pdf')

        except Exception, e:
            print "LATEX PARSER ERROR: %s" % e
            return ("LATEX PARSER ERROR: %s" % e, 'text/html')


from trac.wiki.macros import WikiMacroBase

class TexIncludeMacro(WikiMacroBase):
    """
    This is the Tex version of Image macro.
    """
    def render_macro(self, req, name, content):
        db = self.env.get_db_cnx()

        txt = content or ''
        args = txt.split('|')
        url = args.pop(0).replace("'", "''")

        if ":" in url:
            base, name = url.split(":")
            if base != "wiki":
                return "Use 'wiki:' prefix in wiki page url", base, name
        else:
            name = url

        sql = "SELECT text from wiki where name = '%s' order by version desc limit 1" % name
        cs = db.cursor()
        cs.execute(sql)
        result = cs.fetchone()

        if not result:
            return '<b>Wiki Page %s not found!</b>' % url

        text = result[0]
        out = StringIO()
        TexFormatter(self.env, req, absurls=False, db=None, begindoc=True).format(text, out, False)
        text = out.getvalue()

        return text
